Kompletny przewodnik po s艂owie kluczowym 'infer' w TypeScript, wyja艣niaj膮cy, jak u偶ywa膰 go z typami warunkowymi do wydajnego wyodr臋bniania i manipulacji typami, w tym zaawansowane przypadki u偶ycia.
Opanowanie TypeScript Infer: Warunkowe Wyodr臋bnianie Typ贸w dla Zaawansowanej Manipulacji Typami
System typ贸w TypeScript jest niezwykle pot臋偶ny, umo偶liwiaj膮c programistom tworzenie solidnych i 艂atwych w utrzymaniu aplikacji. Jedn膮 z kluczowych cech umo偶liwiaj膮cych t臋 moc jest s艂owo kluczowe infer
u偶ywane w po艂膮czeniu z typami warunkowymi. To po艂膮czenie zapewnia mechanizm wyodr臋bniania okre艣lonych typ贸w ze z艂o偶onych struktur typ贸w. Ten wpis na blogu zag艂臋bia si臋 w s艂owo kluczowe infer
, wyja艣niaj膮c jego funkcjonalno艣膰 i prezentuj膮c zaawansowane przypadki u偶ycia. Zbadamy praktyczne przyk艂ady maj膮ce zastosowanie w r贸偶nych scenariuszach tworzenia oprogramowania, od interakcji z API po z艂o偶on膮 manipulacj臋 strukturami danych.
Czym s膮 Typy Warunkowe?
Zanim przejdziemy do infer
, szybko przejrzyjmy typy warunkowe. Typy warunkowe w TypeScript umo偶liwiaj膮 zdefiniowanie typu na podstawie warunku, podobnie jak operator tr贸jargumentowy w JavaScript. Podstawowa sk艂adnia to:
T extends U ? X : Y
To czyta si臋 jako: "Je艣li typ T
jest przypisywalny do typu U
, to typem jest X
; w przeciwnym razie typem jest Y
."
Przyk艂ad:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Wprowadzenie do S艂owa Kluczowego infer
S艂owo kluczowe infer
jest u偶ywane w klauzuli extends
typu warunkowego do deklarowania zmiennej typu, kt贸ra mo偶e by膰 wywnioskowana z sprawdzanego typu. Zasadniczo pozwala to "uchwyci膰" cz臋艣膰 typu do p贸藕niejszego wykorzystania.
Podstawowa Sk艂adnia:
type MyType<T> = T extends (infer U) ? U : never;
W tym przyk艂adzie, je艣li T
jest przypisywalne do jakiego艣 typu, TypeScript spr贸buje wywnioskowa膰 typ U
. Je艣li wnioskowanie si臋 powiedzie, typem b臋dzie U
; w przeciwnym razie b臋dzie to never
.
Proste Przyk艂ady U偶ycia infer
1. Wnioskowanie Typu Zwracanego Funkcji
Cz臋stym przypadkiem u偶ycia jest wnioskowanie typu zwracanego funkcji:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number
function greet(name: string): string {
return `Hello, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
W tym przyk艂adzie, ReturnType<T>
przyjmuje typ funkcji T
jako dane wej艣ciowe. Sprawdza, czy T
jest przypisywalne do funkcji, kt贸ra akceptuje dowolne argumenty i zwraca warto艣膰. Je艣li tak, wnioskuje typ zwracany jako R
i go zwraca. W przeciwnym razie zwraca any
.
2. Wnioskowanie Typu Elementu Tablicy
Innym przydatnym scenariuszem jest wyodr臋bnianie typu elementu z tablicy:
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never
Tutaj, ArrayElementType<T>
sprawdza, czy T
jest typem tablicowym. Je艣li tak, wnioskuje typ elementu jako U
i go zwraca. Je艣li nie, zwraca never
.
Zaawansowane Przypadki U偶ycia infer
1. Wnioskowanie Parametr贸w Konstruktora
Mo偶esz u偶y膰 infer
do wyodr臋bnienia typ贸w parametr贸w funkcji konstruktora:
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Person {
constructor(public name: string, public age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]
class Point {
constructor(public x: number, public y: number) {}
}
type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]
W tym przypadku, ConstructorParameters<T>
przyjmuje typ funkcji konstruktora T
. Wnioskuje typy parametr贸w konstruktora jako P
i zwraca je jako krotk臋.
2. Wyodr臋bnianie W艂a艣ciwo艣ci z Typ贸w Obiekt贸w
infer
mo偶e by膰 r贸wnie偶 u偶ywane do wyodr臋bniania okre艣lonych w艂a艣ciwo艣ci z typ贸w obiekt贸w przy u偶yciu typ贸w mapowanych i typ贸w warunkowych:
type PickByType<T, K extends keyof T, U> = {
[P in K as T[P] extends U ? P : never]: T[P];
};
interface User {
id: number;
name: string;
age: number;
email: string;
isActive: boolean;
}
type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }
type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }
//An interface representing geographic coordinates.
interface GeoCoordinates {
latitude: number;
longitude: number;
altitude: number;
country: string;
city: string;
timezone: string;
}
type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }
Tutaj, PickByType<T, K, U>
tworzy nowy typ, kt贸ry zawiera tylko w艂a艣ciwo艣ci T
(z kluczami w K
), kt贸rych warto艣ci s膮 przypisywalne do typu U
. Typ mapowany iteruje po kluczach T
, a typ warunkowy odfiltrowuje klucze, kt贸re nie pasuj膮 do okre艣lonego typu.
3. Praca z Obietnicami
Mo偶esz wywnioskowa膰 rozwi膮zany typ Promise
:
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'Data from API';
}
type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string
async function fetchNumbers(): Promise<number[]> {
return [1, 2, 3];
}
type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]
Typ Awaited<T>
przyjmuje typ T
, kt贸ry ma by膰 Obietnic膮. Nast臋pnie typ wnioskuje rozwi膮zany typ U
Obietnicy i go zwraca. Je艣li T
nie jest obietnic膮, zwraca T. Jest to wbudowany typ narz臋dziowy w nowszych wersjach TypeScript.
4. Wyodr臋bnianie Typu Tablicy Obietnic
Po艂膮czenie wnioskowania typu tablicy i Awaited
pozwala wywnioskowa膰 typ rozwi膮zywany przez tablic臋 Obietnic. Jest to szczeg贸lnie przydatne podczas pracy z Promise.all
.
type PromiseArrayReturnType<T extends Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};
async function getUSDRate(): Promise<number> {
return 0.0069;
}
async function getEURRate(): Promise<number> {
return 0.0064;
}
const rates = [getUSDRate(), getEURRate()];
type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]
Ten przyk艂ad najpierw definiuje dwie funkcje asynchroniczne, getUSDRate
i getEURRate
, kt贸re symuluj膮 pobieranie kurs贸w wymiany. Typ narz臋dziowy PromiseArrayReturnType
nast臋pnie wyodr臋bnia rozwi膮zany typ z ka偶dej Promise
w tablicy, co daje typ krotki, gdzie ka偶dy element jest oczekiwanym typem odpowiedniej Obietnicy.
Praktyczne Przyk艂ady w R贸偶nych Domenach
1. Aplikacja E-commerce
Rozwa偶my aplikacj臋 e-commerce, w kt贸rej pobierasz szczeg贸艂y produktu z API. Mo偶esz u偶y膰 infer
do wyodr臋bnienia typu danych produktu:
interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
category: string;
rating: number;
countryOfOrigin: string;
}
async function fetchProduct(productId: number): Promise<Product> {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Example Product',
price: 29.99,
description: 'A sample product',
imageUrl: 'https://example.com/image.jpg',
category: 'Electronics',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Product Name: ${product.name}`);
console.log(`Price: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
W tym przyk艂adzie definiujemy interfejs Product
i funkcj臋 fetchProduct
, kt贸ra pobiera szczeg贸艂y produktu z API. U偶ywamy Awaited
i ReturnType
do wyodr臋bnienia typu Product
z typu zwracanego funkcji fetchProduct
, co pozwala nam typowa膰 funkcj臋 displayProductDetails
.
2. Internacjonalizacja (i18n)
Za艂贸偶my, 偶e masz funkcj臋 t艂umaczenia, kt贸ra zwraca r贸偶ne ci膮gi znak贸w w zale偶no艣ci od ustawie艅 regionalnych. Mo偶esz u偶y膰 infer
do wyodr臋bnienia typu zwracanego tej funkcji w celu zapewnienia bezpiecze艅stwa typ贸w:
interface Translations {
greeting: string;
farewell: string;
welcomeMessage: (name: string) => string;
}
const enTranslations: Translations = {
greeting: 'Hello',
farewell: 'Goodbye',
welcomeMessage: (name: string) => `Welcome, ${name}!`,
};
const frTranslations: Translations = {
greeting: 'Bonjour',
farewell: 'Au revoir',
welcomeMessage: (name: string) => `Bienvenue, ${name}!`,
};
function getTranslation(locale: 'en' | 'fr'): Translations {
return locale === 'en' ? enTranslations : frTranslations;
}
type TranslationType = ReturnType<typeof getTranslation>;
function greetUser(locale: 'en' | 'fr', name: string) {
const translations = getTranslation(locale);
console.log(translations.welcomeMessage(name));
}
greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!
Tutaj, TranslationType
jest wywnioskowany jako interfejs Translations
, zapewniaj膮c, 偶e funkcja greetUser
ma poprawne informacje o typie do uzyskiwania dost臋pu do przet艂umaczonych ci膮g贸w znak贸w.
3. Obs艂uga Odpowiedzi API
Podczas pracy z API struktura odpowiedzi mo偶e by膰 z艂o偶ona. infer
mo偶e pom贸c w wyodr臋bnieniu okre艣lonych typ贸w danych z zagnie偶d偶onych odpowiedzi API:
interface ApiResponse<T> {
status: number;
data: T;
message?: string;
}
interface UserData {
id: number;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
country: string;
language: string;
}
}
async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: 200,
data: {
id: userId,
username: 'johndoe',
email: 'john.doe@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
country: 'USA',
language: 'en'
}
}
});
}, 500);
});
}
type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;
type UserProfileType = UserApiResponse['data']['profile'];
function displayUserProfile(profile: UserProfileType) {
console.log(`Name: ${profile.firstName} ${profile.lastName}`);
console.log(`Country: ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
W tym przyk艂adzie definiujemy interfejs ApiResponse
i interfejs UserData
. U偶ywamy infer
i indeksowania typ贸w do wyodr臋bnienia UserProfileType
z odpowiedzi API, zapewniaj膮c, 偶e funkcja displayUserProfile
otrzyma poprawny typ.
Najlepsze Praktyki U偶ywania infer
- Zachowaj Prostot臋: U偶ywaj
infer
tylko wtedy, gdy jest to konieczne. Nadmierne u偶ywanie go mo偶e utrudni膰 czytanie i zrozumienie kodu. - Dokumentuj Swoje Typy: Dodaj komentarze, aby wyja艣ni膰, co robi膮 twoje typy warunkowe i instrukcje
infer
. - Testuj Swoje Typy: U偶yj sprawdzania typ贸w TypeScript, aby upewni膰 si臋, 偶e twoje typy zachowuj膮 si臋 zgodnie z oczekiwaniami.
- Rozwa偶 Wydajno艣膰: Z艂o偶one typy warunkowe mog膮 czasami wp艂ywa膰 na czas kompilacji. Pami臋taj o z艂o偶ono艣ci swoich typ贸w.
- U偶ywaj Typ贸w Narz臋dziowych: TypeScript udost臋pnia kilka wbudowanych typ贸w narz臋dziowych (np.
ReturnType
,Awaited
), kt贸re mog膮 upro艣ci膰 kod i zmniejszy膰 potrzeb臋 stosowania niestandardowych instrukcjiinfer
.
Cz臋ste Pu艂apki
- Niepoprawne Wnioskowanie: Czasami TypeScript mo偶e wywnioskowa膰 typ, kt贸ry nie jest tym, czego oczekujesz. Sprawd藕 dok艂adnie swoje definicje typ贸w i warunki.
- Cykliczne Zale偶no艣ci: Zachowaj ostro偶no艣膰 podczas definiowania typ贸w rekurencyjnych za pomoc膮
infer
, poniewa偶 mog膮 one prowadzi膰 do cyklicznych zale偶no艣ci i b艂臋d贸w kompilacji. - Zbyt Z艂o偶one Typy: Unikaj tworzenia zbyt z艂o偶onych typ贸w warunkowych, kt贸re s膮 trudne do zrozumienia i utrzymania. Rozbij je na mniejsze, 艂atwiejsze do zarz膮dzania typy.
Alternatywy dla infer
Chocia偶 infer
jest pot臋偶nym narz臋dziem, istniej膮 sytuacje, w kt贸rych bardziej odpowiednie mog膮 by膰 alternatywne podej艣cia:
- Asercje Typ贸w: W niekt贸rych przypadkach mo偶esz u偶y膰 asercji typ贸w, aby jawnie okre艣li膰 typ warto艣ci zamiast go wnioskowa膰. Nale偶y jednak zachowa膰 ostro偶no艣膰 przy u偶ywaniu asercji typ贸w, poniewa偶 mog膮 one pomin膮膰 sprawdzanie typ贸w.
- Stra偶nicy Typ贸w: Stra偶nicy typ贸w mog膮 by膰 u偶ywani do zaw臋偶ania typu warto艣ci na podstawie kontroli w czasie wykonywania. Jest to przydatne, gdy musisz obs艂ugiwa膰 r贸偶ne typy na podstawie warunk贸w w czasie wykonywania.
- Typy Narz臋dziowe: TypeScript udost臋pnia bogaty zestaw typ贸w narz臋dziowych, kt贸re mog膮 obs艂ugiwa膰 wiele typowych zada艅 manipulacji typami bez potrzeby stosowania niestandardowych instrukcji
infer
.
Wniosek
S艂owo kluczowe infer
w TypeScript, w po艂膮czeniu z typami warunkowymi, odblokowuje zaawansowane mo偶liwo艣ci manipulacji typami. Pozwala wyodr臋bnia膰 okre艣lone typy ze z艂o偶onych struktur typ贸w, umo偶liwiaj膮c pisanie bardziej solidnego, 艂atwego w utrzymaniu i bezpiecznego pod wzgl臋dem typ贸w kodu. Od wnioskowania typ贸w zwracanych funkcji po wyodr臋bnianie w艂a艣ciwo艣ci z typ贸w obiekt贸w, mo偶liwo艣ci s膮 ogromne. Rozumiej膮c zasady i najlepsze praktyki przedstawione w tym przewodniku, mo偶esz wykorzysta膰 infer
w pe艂ni i podnie艣膰 swoje umiej臋tno艣ci TypeScript. Pami臋taj, aby dokumentowa膰 swoje typy, dok艂adnie je testowa膰 i rozwa偶a膰 alternatywne podej艣cia, gdy jest to w艂a艣ciwe. Opanowanie infer
umo偶liwia pisanie naprawd臋 ekspresyjnego i pot臋偶nego kodu TypeScript, co ostatecznie prowadzi do lepszego oprogramowania.